package com.yxx.aspect;

import cn.hutool.extra.servlet.ServletUtil;
import com.alibaba.fastjson2.JSON;
import com.yxx.entity.ExceptionLog;
import com.yxx.manager.AsyncManager;
import com.yxx.manager.factory.AsyncFactory;
import com.yxx.utils.IpUtils;
import com.yxx.utils.UserAgentUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Objects;

/**
 * AOP 记录异常日志
 * @author yuxiangxun
 * @date 2025/2/15
 * @apiNote
 */
@Aspect
@Component
public class ExceptionLogAspect {

    /**
     * 设置操作异常日志的切入点，扫描所有controller包下的方法
     */
    @Pointcut("execution(* com.yxx.controller..*.*(..))")
    public void ExceptionPointCut(){}

    /**
     * 异常通知，只有连接点异常才执行
     * @param joinPoint 切面方法信息
     * @param ex        异常
     */
    @AfterThrowing(pointcut = "ExceptionPointCut()", throwing = "ex")
    public void doAfterThrowing(JoinPoint joinPoint, Throwable ex){
        // 从切面切入点处通过反射机制获取织入点处的方法
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        // 获取织入点所在的方法
        Method method = methodSignature.getMethod();
        // 获取 request
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest httpServletRequest = Objects.requireNonNull(servletRequestAttributes).getRequest();

        // 获取操作
        // 获取 API 注解 -> Swagger 注解 -> 操作
        Class<?> declaringType = methodSignature.getDeclaringType();
        Api api = (Api) declaringType.getAnnotation(Api.class);
        // 获取 ApiOperation 注解
        ApiOperation apiOperation = (ApiOperation) methodSignature.getDeclaringType().getAnnotation(ApiOperation.class);

        // 封装数据
        ExceptionLog exceptionLog = new ExceptionLog();
        exceptionLog.setModule(api.tags()[0]);
        exceptionLog.setUri(httpServletRequest.getRequestURI());
        // 通过异常的运行时类，返回Class对象（异常对象的类对象），从中获取异常类的名称
        exceptionLog.setName(ex.getClass().getName());
        exceptionLog.setDescription(apiOperation.value());

        // 获取请求中的类名
        String className = joinPoint.getTarget().getClass().getName();
        // 获取请求中的方法名称
        String methodName = method.getName();
        methodName = className + "." + methodName;
        // 这里应该时记录出现异常的 请求方法 的 “全限定类.方法名”
        exceptionLog.setErrorMethod(methodName);
        // 异常信息
        exceptionLog.setMessage(stackTraceToString(ex.getClass().getName(),ex.getMessage(),ex.getStackTrace()));
        // 请求参数
        if(joinPoint.getArgs()[0] instanceof MultipartFile){
            exceptionLog.setParams(((MultipartFile) joinPoint.getArgs()[0]).getOriginalFilename());
        }else {
            exceptionLog.setParams(JSON.toJSONString(joinPoint.getArgs()));
        }
        
        // ip地址和操作地址
        String ipAddress = ServletUtil.getClientIP(httpServletRequest);
        exceptionLog.setIpAddress(ipAddress);
        exceptionLog.setIpSource(IpUtils.getIpSource(ipAddress));
        
        // 操作系统和浏览器
        Map<String, String> userAgentMap = UserAgentUtils.parseOsAndBrowser(httpServletRequest.getHeader("User-Agent"));
        exceptionLog.setOs(userAgentMap.get("os"));
        exceptionLog.setBrowser(userAgentMap.get("browser"));

        // 保存到数据库
        AsyncManager.getInstance().execute(AsyncFactory.recordException(exceptionLog));
    }

    private String stackTraceToString(String exceptionName, String exceptionMessage, StackTraceElement[] elements){
        StringBuilder stringBuilder = new StringBuilder();
        for(StackTraceElement stet : elements){
            stringBuilder.append(stet).append("\n");
        }
        return exceptionName + ":" + exceptionMessage + "\n" + stringBuilder;
    }
}
